/*
 * Written By Charles M. Chen 
 * 
 * Created on Sep 2, 2005
 *
 */

package org.cmc.music.myid3.id3v2;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;

import org.cmc.music.myid3.MyID3Listener;
import org.cmc.music.myid3.UnicodeMetrics;
import org.cmc.music.util.Debug;

public class MyID3v2Read implements MyID3v2Constants
{

	private static final int HIGH_BIT = 1 << 7;

	private final InputStream is;
	private final boolean async;
	private final MyID3Listener listener;

	public MyID3v2Read(final MyID3Listener listener, final InputStream is,
			boolean async)
	{
		this.listener = listener;
		this.is = is;
		this.async = async;
	}

	private boolean complete = false, error = false, noTag = false,
			streamComplete = false;

	public void dump()
	{
		Debug.debug("complete", complete);
		Debug.debug("error", error);
		Debug.debug("no_tag", noTag);

		Debug.debug("error_msg", errorMessage);
		Debug.debug("stream_complete", streamComplete);

		Debug.debug("index", index);
		Debug.debug("last", last);
		Debug.debug("header_read", header_read);
		Debug.debug("tag_read", tagRead);
		Debug.debug("bytes_read", bytes_read);
		Debug.debug("tagLength", tagLength);
		Debug.debug("tags", frames);
	}

	public boolean isComplete()
	{
		return complete || error || noTag;
	}

	public boolean isError()
	{
		return error;
	}

	public boolean hasTags()
	{
		return !error && complete && !noTag;
	}

	// public boolean isSuccess()
	// {
	// return tag_read && !error;
	// }

	private boolean header_read = false, tagRead = false;
	private int index = 0, last = -1;

	public boolean iteration()
	{
		if (isComplete())
			return true;

		if (!read())
			return false;

		if (isComplete())
			return true;

		if (!header_read)
		{
			if (bytes_read < TAG_HEADER_LENGTH)
			{
				if (streamComplete)
					error = true;
				return true;
			}
			readHeader();
		}

		if (!tagRead)
		{
			if (bytes_read < tagLength)
			{
				if (streamComplete)
					error = true;
				return true;
			}
			readTag();

			complete = true;

			// Debug.debug("finished v2 file_precis", file_precis);
			// if (file_precis != null)
			// {
			// file_precis.setID3v2(getBytes(), getTags());
			// }
		}

		// complete = true;

		return true;
	}

	private int readInt3(byte bytes[], boolean check_tagLength)
	{
		if (((index + 2) >= tagLength) && check_tagLength)
		{
			setError("readInt3(index: " + index + ", tagLength: " + tagLength);
			return -1;
		}
		if ((index + 3) >= bytes.length)
		{
			setError("readInt3(index: " + index + ", bytes.length: "
					+ bytes.length);
			return -1;
		}

		int array[] = { 0xff & bytes[index++], //
				0xff & bytes[index++], //
				0xff & bytes[index++], //
		};

		int result = (array[0] << 16) | (array[1] << 8) | (array[2] << 0);
		return result;
	}

	public static Number readSynchsafeInt(byte bytes[], int start)
	{
		if ((start + 3) >= bytes.length)
		{
			// setError("readSynchsafeInt(index: " + start + ", bytes.length: "
			// + bytes.length);
			return null;
		}

		int index = start;
		int array[] = { 0xff & bytes[index++], //
				0xff & bytes[index++], //
				0xff & bytes[index++], //
				0xff & bytes[index++], //
		};

		for (int i = 0; i < array.length; i++)
		{
			if ((array[i] & HIGH_BIT) > 0)
			{
				array[i] &= HIGH_BIT;
			}
		}

		int result = (array[0] << 21) | (array[1] << 14) | (array[2] << 7)
				| (array[3] << 0);
		return new Integer(result);
	}

	private int readSynchsafeInt(byte bytes[], boolean check_tagLength)
	{
		if (((index + 3) >= tagLength) && check_tagLength)
		{
			setError("readSynchsafeInt(index: " + index + ", tagLength: "
					+ tagLength);
			return -1;
		}
		if ((index + 3) >= bytes.length)
		{
			setError("readSynchsafeInt(index: " + index + ", bytes.length: "
					+ bytes.length);
			return -1;
		}

		int array[] = { 0xff & bytes[index++], //
				0xff & bytes[index++], //
				0xff & bytes[index++], //
				0xff & bytes[index++], //
		};

		for (int i = 0; i < array.length; i++)
		{

			if ((array[i] & HIGH_BIT) > 0)
			{
				array[i] &= HIGH_BIT;
			}
		}

		int result = (array[0] << 21) | (array[1] << 14) | (array[2] << 7)
				| (array[3] << 0);
		return result;
	}

	private int readInt(byte bytes[], boolean check_tagLength)
	{
		if (((index + 3) >= tagLength) && check_tagLength)
		{
			setError("readInt(index: " + index + ", tagLength: " + tagLength);
			return -1;
		}
		if ((index + 3) >= bytes.length)
		{
			setError("readInt(index: " + index + ", bytes.length: "
					+ bytes.length);
			return -1;
		}

		int array[] = { 0xff & bytes[index++], //
				0xff & bytes[index++], //
				0xff & bytes[index++], //
				0xff & bytes[index++], //
		};

		int result = (array[0] << 24) | (array[1] << 16) | (array[2] << 8)
				| (array[3] << 0);
		return result;
	}

	private int readShort(byte bytes[])
	{
		if (((index + 1) >= tagLength) || ((index + 1) >= bytes.length))
		{
			setError("readShort(index: " + index + ", tagLength: " + tagLength
					+ ", bytes.length: " + bytes.length);
			Debug.debug("bad readShort index", index);
			Debug.debug("bytes", bytes, index);
			Debug.dumpStack(5);

			return -1;
		}
		byte array[] = { bytes[index++], //
				bytes[index++], //
		};

		int result = (array[0] << 8) | (array[1] << 0);
		return result;
	}

	private byte versionMajor, versionMinor;
	private boolean tagUnsynchronization = false, tagCompression = false,
			tagExtendedHeader = false, tagExperimentalIndicator = false,
			tagFooterPresent = false;

	private void readHeader()
	{
		byte bytes[] = baos.toByteArray();
		if (bytes.length < 10)
		{
			setError("missing header");
			return;
		}

		if (listener != null)
			listener.log("id3v2 header");

		if (bytes[index++] != 0x49)
			noTag = true;
		else if (bytes[index++] != 0x44)
			noTag = true;
		else if (bytes[index++] != 0x33)
			noTag = true;

		if (error || noTag)
			return;

		versionMajor = bytes[index++];
		versionMinor = bytes[index++];

		if (listener != null)
		{
			listener.log("\t" + "id3v2 versionMajor", versionMajor);
			listener.log("\t" + "id3v2 versionMinor", versionMinor);
		}

		if ((versionMajor < 2) || (versionMajor > 4))
		{
			setError("Unknown id3v2 Major Version: " + versionMajor);
			return;
		}
		long flags = bytes[index++];
		long workingFlags = flags;

		if (versionMajor == 2)
		{
			if ((workingFlags & HEADER_FLAG_ID3v22_UNSYNCHRONISATION) > 0)
			{
				tagUnsynchronization = true;
				workingFlags ^= HEADER_FLAG_ID3v22_UNSYNCHRONISATION;
			}
			if ((workingFlags & HEADER_FLAG_ID3v22_COMPRESSION) > 0)
			{
				tagCompression = true;
				workingFlags ^= HEADER_FLAG_ID3v22_COMPRESSION;
			}
		} else if (versionMajor == 3)
		{
			if ((workingFlags & HEADER_FLAG_ID3v23_UNSYNCHRONISATION) > 0)
			{
				tagUnsynchronization = true;
				workingFlags ^= HEADER_FLAG_ID3v23_UNSYNCHRONISATION;
			}
			if ((workingFlags & HEADER_FLAG_ID3v23_EXTENDED_HEADER) > 0)
			{
				tagExtendedHeader = true;
				workingFlags ^= HEADER_FLAG_ID3v23_EXTENDED_HEADER;
			}
			if ((workingFlags & HEADER_FLAG_ID3v23_EXPERIMENTAL_INDICATOR) > 0)
			{
				tagExperimentalIndicator = true;
				workingFlags ^= HEADER_FLAG_ID3v23_EXPERIMENTAL_INDICATOR;
			}

			// hack to fix old mistake.
			if ((workingFlags & HEADER_FLAG_ID3v24_FOOTER_PRESENT) > 0)
				workingFlags ^= HEADER_FLAG_ID3v24_FOOTER_PRESENT;

		} else if (versionMajor == 4)
		{
			if ((workingFlags & HEADER_FLAG_ID3v24_UNSYNCHRONISATION) > 0)
			{
				tagUnsynchronization = true;
				workingFlags ^= HEADER_FLAG_ID3v24_UNSYNCHRONISATION;
			}
			if ((workingFlags & HEADER_FLAG_ID3v24_EXTENDED_HEADER) > 0)
			{
				tagExtendedHeader = true;
				workingFlags ^= HEADER_FLAG_ID3v24_EXTENDED_HEADER;
			}
			if ((workingFlags & HEADER_FLAG_ID3v24_EXPERIMENTAL_INDICATOR) > 0)
			{
				tagExperimentalIndicator = true;
				workingFlags ^= HEADER_FLAG_ID3v24_EXPERIMENTAL_INDICATOR;
			}
			if ((workingFlags & HEADER_FLAG_ID3v24_FOOTER_PRESENT) > 0)
			{
				tagFooterPresent = true;
				workingFlags ^= HEADER_FLAG_ID3v24_FOOTER_PRESENT;
			}
		} else
		{
			setError("Unknown id3v2 Major Version: " + versionMajor);
			return;
		}
		if (workingFlags > 0)
		{
			setError("Unknown id3v2 tag flags(id3v2 version: " + versionMajor
					+ "): " + Long.toHexString(flags));
			return;
		}

		if (listener != null)
		{
			listener.log("\t" + "unsynchronization", tagUnsynchronization);
			listener.log("\t" + "compression", tagCompression);
			listener.log("\t" + "extendedHeader", tagExtendedHeader);
			listener.log("\t" + "experimentalIndicator",
					tagExperimentalIndicator);
			listener.log("\t" + "footerPresent", tagFooterPresent);
		}

		{
			tagLength = readSynchsafeInt(bytes, false);

			tagLength += 10;
			last = tagLength;
			if (tagFooterPresent)
				tagLength += 10;
		}

		header_read = true;
		if (index != TAG_HEADER_LENGTH)
			setError("index!=kHEADER_SIZE");

		if (listener != null)
		{
			listener.log("\t" + "tagLength", tagLength);
			listener.log();
		}
	}

	// private boolean extended_header;
	private final Vector frames = new Vector();

	private byte[] ununsynchronize(byte bytes[])
	{
		// Debug.debug("ununsynchronize before", bytes.length);

		ByteArrayOutputStream result = new ByteArrayOutputStream();
		int i = 0;
		for (; i < bytes.length;)
		{
			byte b = bytes[i++];
			result.write(b);
			if ((0xff & b) != 0xff)
				continue;

			if (i >= bytes.length)
				break;

			// look ahead.
			byte b1 = bytes[i];
			if ((0xff & b1) == 0)
				i++;
		}
		bytes = result.toByteArray();
		// Debug.debug("ununsynchronize after", bytes.length);
		return bytes;
	}

	private static final String LEGAL_FRAME_ID_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

	private String parseFrameID(byte bytes[])
	{
		for (int i = 0; i < bytes.length; i++)
		{
			int b = 0xff & bytes[i];
			char c = (char) b;
			if (LEGAL_FRAME_ID_CHARACTERS.indexOf(c) < 0)
			{
				setError("invalid id3v2 frame id byte: "
						+ Integer.toHexString(b));
				return null;
			}
		}
		return new String(bytes);
	}

	private boolean isZeroFrameId(byte bytes[])
	{
		for (int i = 0; i < bytes.length; i++)
		{
			if ((0xff & bytes[i]) > 0)
				return false;
		}
		return true;
	}

	private void readTag()
	{
		byte bytes[] = baos.toByteArray();
		if (bytes.length < tagLength)
		{
			setError("missing tag");
			return;
		}

		if (tagUnsynchronization)
			bytes = ununsynchronize(bytes);

		if (tagExtendedHeader)
			index += 4;

		int tagCount = 0;
		for (int tag_num = 0; (((index + 7) < last) && (!error)); tag_num++)
		{
			if ((index + 7) >= last)
				break;

			byte frameID[];

			if (versionMajor >= 3)
			{
				frameID = new byte[] { bytes[index++], //
						bytes[index++], //
						bytes[index++], //
						bytes[index++], //
				};
			} else
			{
				frameID = new byte[] { bytes[index++], //
						bytes[index++], //
						bytes[index++], //
				};
			}
			if (isZeroFrameId(frameID))
			{
				// Not a frame, rest of the tag is padding.
				if (listener != null)
					listener.log("zero frameID", frameID);
				break;
			}
			String frameIDString = parseFrameID(frameID);
			if (null == frameIDString)
				break;

			if (listener != null)
				listener.log("id3v2 frameIDString", frameIDString);

			int frameLength;
			if (versionMajor >= 4)
				frameLength = readSynchsafeInt(bytes, true);
			else if (versionMajor >= 3)
				frameLength = readInt(bytes, true);
			else
				frameLength = readInt3(bytes, true);

			if (listener != null)
				listener.log("frameLength", frameLength);

			int maxTagLength = tagLength - index;
			if (versionMajor >= 3)
				maxTagLength += 2;

			if (frameLength == 0)
			{
				if (listener != null)
					listener.log("frame has zero length.");
				break;
			}

			if ((frameLength > maxTagLength) || (frameLength < 0))
			{
				if (listener != null)
				{
					listener
							.log("frame length exceeds tag length", frameLength);
					listener.log("bad frameLength versionMajor", versionMajor);
					listener.log("bad frameLength versionMinor", versionMinor);
					listener
							.log("bad frameLength frameIDString", frameIDString);
					listener.log("bad frameLength maxTagLength", maxTagLength);
					listener.log("bad frameLength frameLength", frameLength
							+ " (0x" + Integer.toHexString(frameLength) + ")");
					listener.log("bad frameLength tagLength", tagLength
							+ " (0x" + Integer.toHexString(tagLength) + ")");
					listener.log("bad frameLength index", index);

					listener.log("bytes", bytes);
				}

				setError("bad frame length(" + tag_num + ": " + frameIDString
						+ "): " + frameLength + " (" + new String(frameID));

				break;
			}

			ID3v2FrameFlags flags = null;
			if (versionMajor == 3 || versionMajor == 4)
			{
				int value = readShort(bytes);
				int workingFlags = value;

				flags = new ID3v2FrameFlags();

				if (versionMajor == 3)
				{
					if ((workingFlags & FRAME_FLAG_ID3v23_TAG_ALTER_PRESERVATION) > 0)
					{
						flags.setTagAlterPreservation(true);
						workingFlags ^= FRAME_FLAG_ID3v23_TAG_ALTER_PRESERVATION;
					}
					if ((workingFlags & FRAME_FLAG_ID3v23_FILE_ALTER_PRESERVATION) > 0)
					{
						flags.setFileAlterPreservation(true);
						workingFlags ^= FRAME_FLAG_ID3v23_FILE_ALTER_PRESERVATION;
					}
					if ((workingFlags & FRAME_FLAG_ID3v23_READ_ONLY) > 0)
					{
						flags.setReadOnly(true);
						workingFlags ^= FRAME_FLAG_ID3v23_READ_ONLY;
					}
					if ((workingFlags & FRAME_FLAG_ID3v23_GROUPING_IDENTITY) > 0)
					{
						flags.setGroupingIdentity(true);
						workingFlags ^= FRAME_FLAG_ID3v23_GROUPING_IDENTITY;
					}
					if ((workingFlags & FRAME_FLAG_ID3v23_COMPRESSION) > 0)
					{
						flags.setCompression(true);
						workingFlags ^= FRAME_FLAG_ID3v23_COMPRESSION;
					}
					if ((workingFlags & FRAME_FLAG_ID3v23_ENCRYPTION) > 0)
					{
						flags.setEncryption(true);
						workingFlags ^= FRAME_FLAG_ID3v23_ENCRYPTION;
					}
				} else if (versionMajor == 4)
				{

					if ((workingFlags & FRAME_FLAG_ID3v24_TAG_ALTER_PRESERVATION) > 0)
					{
						flags.setTagAlterPreservation(true);
						workingFlags ^= FRAME_FLAG_ID3v24_TAG_ALTER_PRESERVATION;
					}
					if ((workingFlags & FRAME_FLAG_ID3v24_FILE_ALTER_PRESERVATION) > 0)
					{
						flags.setFileAlterPreservation(true);
						workingFlags ^= FRAME_FLAG_ID3v24_FILE_ALTER_PRESERVATION;
					}
					if ((workingFlags & FRAME_FLAG_ID3v24_READ_ONLY) > 0)
					{
						flags.setReadOnly(true);
						workingFlags ^= FRAME_FLAG_ID3v24_READ_ONLY;
					}
					if ((workingFlags & FRAME_FLAG_ID3v24_GROUPING_IDENTITY) > 0)
					{
						flags.setGroupingIdentity(true);
						workingFlags ^= FRAME_FLAG_ID3v24_GROUPING_IDENTITY;
					}
					if ((workingFlags & FRAME_FLAG_ID3v24_COMPRESSION) > 0)
					{
						flags.setCompression(true);
						workingFlags ^= FRAME_FLAG_ID3v24_COMPRESSION;
					}
					if ((workingFlags & FRAME_FLAG_ID3v24_ENCRYPTION) > 0)
					{
						flags.setEncryption(true);
						workingFlags ^= FRAME_FLAG_ID3v24_ENCRYPTION;
					}
					if ((workingFlags & FRAME_FLAG_ID3v24_UNSYNCHRONISATION) > 0)
					{
						flags.setUnsynchronisation(true);
						workingFlags ^= FRAME_FLAG_ID3v24_UNSYNCHRONISATION;
					}
					if ((workingFlags & FRAME_FLAG_ID3v24_DATA_LENGTH_INDICATOR) > 0)
					{
						flags.setDataLengthIndicator(true);
						workingFlags ^= FRAME_FLAG_ID3v24_DATA_LENGTH_INDICATOR;
					}
				}

				if (workingFlags > 0)
				{
					setError("Unknown id3v2 frame flags(id3v2 version: "
							+ versionMajor + "): " + Long.toHexString(value));
					return;
				}
			} else if (versionMajor == 2)
			{
				flags = new ID3v2FrameFlags();
			} else
			{
				setError("Unknown ID3v2 version: " + versionMajor);
				return;
			}

			if (listener != null)
				listener.log("flags", flags.getSummary());

			if (frameLength > 0)
			{
				int dataLengthIndicator = -1;
				if (flags != null && flags.getDataLengthIndicator())
				{
					dataLengthIndicator = readSynchsafeInt(bytes, true);
					frameLength -= 4;
					if (listener != null)
						listener
								.log("dataLengthIndicator", dataLengthIndicator);
				}

				byte frameBytes[] = new byte[frameLength];

				System.arraycopy(bytes, index, frameBytes, 0, frameLength);
				index += frameLength;

				if (flags != null && flags.getUnsynchronisation())
					frameBytes = ununsynchronize(frameBytes);

				try
				{
					if (frameID[0] == 'T')
					{
						if (listener != null)
							listener.log("text frame");
						readTextTag(frameLength, frameID, frameBytes,
								frameIDString);
					} else
					{
						if (listener != null)
							listener.log("data frame");
						readDataTag(frameLength, frameID, frameBytes,
								frameIDString, flags);
					}
				} catch (IOException e)
				{
					if (listener != null)
						listener.log("IOException", e.getMessage());
					setError(e.getMessage());

					// TODO: return or break here or what?
					return;
				}
			}
			tagCount++;

			if (listener != null)
				listener.log();
		}

		tagRead = true;

		if (listener != null)
			listener.log();

	}

	private void readDataTag(int frameLength, byte frameID[],
			byte frameBytes[], String frameIDString, ID3v2FrameFlags flags)
			throws IOException
	{
		if (frameIDString.equals("COMM") || frameIDString.equals("COM"))
		{
			if (frameBytes.length < 5)
			{
				setError("Unexpected COMM frame length(1): " + frameLength
						+ " (" + new String(frameID));
				return;
			}
			int frameIndex = 0;
			int charEncodingCode = 0xff & frameBytes[frameIndex++];
			byte language_1 = frameBytes[frameIndex++];
			byte language_2 = frameBytes[frameIndex++];
			byte language_3 = frameBytes[frameIndex++];

			String summary = readString(frameBytes, frameIndex,
					charEncodingCode);

			int stringDataLength = findStringDataLength(frameBytes, frameIndex,
					charEncodingCode);
			frameIndex += stringDataLength;

			String comment;
			comment = readString(frameBytes, frameIndex, charEncodingCode);

			MyID3v2FrameText tag = new MyID3v2FrameText(frameIDString,
					frameBytes, comment);
			frames.add(tag);
		} else if (frameIDString.equals("PIC") || frameIDString.equals("APIC"))
		{
			int frameIndex = 0;
			int charEncodingCode = 0xff & frameBytes[frameIndex++];

			String mimeType;
			if (frameIDString.equals("PIC"))
			{
				int imageFormat1 = 0xff & frameBytes[frameIndex++];
				int imageFormat2 = 0xff & frameBytes[frameIndex++];
				int imageFormat3 = 0xff & frameBytes[frameIndex++];

				String extension = "" + (char) imageFormat1
						+ (char) imageFormat2 + (char) imageFormat3;

				mimeType = extension.toLowerCase();
				if (!mimeType.startsWith("image/"))
					mimeType = "image/" + mimeType;
			} else
			{
				mimeType = readString(frameBytes, frameIndex, charEncodingCode);

				int stringDataLength = findStringDataLength(frameBytes,
						frameIndex, charEncodingCode);
				frameIndex += stringDataLength;
			}
			// Debug.debug("PIC imageFormat1", imageFormat1);
			// Debug.debug("PIC imageFormat2", imageFormat2);
			// Debug.debug("PIC imageFormat3", imageFormat3);

			int pictureType = 0xff & frameBytes[frameIndex++];
			// Debug.debug("PIC pictureType", pictureType);

			String description;
			{
				description = readString(frameBytes, frameIndex,
						charEncodingCode);

				int stringDataLength = findStringDataLength(frameBytes,
						frameIndex, charEncodingCode);
				frameIndex += stringDataLength;
			}
			byte imageData[] = new byte[frameBytes.length - frameIndex];
			System.arraycopy(frameBytes, frameIndex, imageData, 0,
					imageData.length);

			frames.add(new MyID3v2FrameImage(frameIDString, frameBytes, flags,
					imageData, mimeType, description, pictureType));
		} else if (frameIDString.equals("PRIV"))
		{
			int frameIndex = 0;
			String owner_identifier;
			{
				byte charEncodingCode = CHAR_ENCODING_CODE_ISO_8859_1;
				owner_identifier = readString(frameBytes, frameIndex,
						charEncodingCode);

				int stringDataLength = findStringDataLength(frameBytes,
						frameIndex, charEncodingCode);
				frameIndex += stringDataLength;
			}
			if (owner_identifier.startsWith("WM/"))
				return;

		} else
			frames.add(new MyID3v2FrameData(frameIDString, frameBytes, flags));
	}

	private void readTextTag(int frameLength, byte frameID[],
			byte frameBytes[], String frameIDString) throws IOException
	{
		if (frameLength == 1)
		{
		} else if (frameLength < 2)
		{
			setError("Unexpected frame length(1): " + frameLength + " ("
					+ new String(frameID));
		} else
		{
			int charEncodingCode = 0xff & frameBytes[0];

			int frameIndex = 1;
			String value = readString(frameBytes, frameIndex, charEncodingCode);

			if (listener != null)
				listener.logWithLength("value", value);

			MyID3v2FrameText tag;

			String value2 = null;
			if (frameIDString.equals("TXXX"))
			{
				int stringDataLength = findStringDataLength(frameBytes,
						frameIndex, charEncodingCode);
				frameIndex += stringDataLength;

				value2 = readString(frameBytes, frameIndex, charEncodingCode);

				if (listener != null)
					listener.logWithLength("value2", value2);

				tag = new MyID3v2FrameText(frameIDString, frameBytes, value,
						value2);
			} else
				tag = new MyID3v2FrameText(frameIDString, frameBytes, value);

			frames.add(tag);
		}
	}

	private String getCharacterEncodingName(int charEncodingCode)
			throws IOException
	{
		switch (charEncodingCode)
		{
		case CHAR_ENCODING_CODE_ISO_8859_1:
			return CHAR_ENCODING_ISO;
		case CHAR_ENCODING_CODE_UTF_16_WITH_BOM:
			return CHAR_ENCODING_UTF_16;
		case CHAR_ENCODING_CODE_UTF_16_NO_BOM:
			return CHAR_ENCODING_UTF_16;
		case CHAR_ENCODING_CODE_UTF_8:
			return CHAR_ENCODING_UTF_8;
		default:
			throw new IOException("Unknown charEncodingCode: "
					+ charEncodingCode);
		}
	}

	private String getCharacterEncodingFullName(int charEncodingCode)
			throws IOException
	{
		switch (charEncodingCode)
		{
		case CHAR_ENCODING_CODE_ISO_8859_1:
			return CHAR_ENCODING_ISO;
		case CHAR_ENCODING_CODE_UTF_16_WITH_BOM:
			return CHAR_ENCODING_UTF_16_WITH_BOM;
		case CHAR_ENCODING_CODE_UTF_16_NO_BOM:
			return CHAR_ENCODING_UTF_16_WITHOUT_BOM;
		case CHAR_ENCODING_CODE_UTF_8:
			return CHAR_ENCODING_UTF_8;
		default:
			throw new IOException("Unknown charEncodingCode: "
					+ charEncodingCode);
		}
	}

	private String readString(byte bytes[], int start, int charEncodingCode)
			throws IOException
	{
		if (listener != null)
			listener.log("reading string with encoding",
					getCharacterEncodingFullName(charEncodingCode));

		UnicodeMetrics unicodeMetrics = UnicodeMetrics
				.getInstance(charEncodingCode);
		int unicodeMetricsEnd = unicodeMetrics.findEndWithoutTerminator(bytes,
				start);
		int unicodeMetricsLength = unicodeMetricsEnd - start;

		String charsetName = getCharacterEncodingName(charEncodingCode);
		return new String(bytes, start, unicodeMetricsLength, charsetName);
	}

	private int findStringDataLength(byte bytes[], int start,
			int charEncodingCode) throws IOException
	{
		UnicodeMetrics unicodeMetrics = UnicodeMetrics
				.getInstance(charEncodingCode);
		int unicodeMetricsEnd = unicodeMetrics.findEndWithTerminator(bytes,
				start);
		int unicodeMetricsLength = unicodeMetricsEnd - start;
		return unicodeMetricsLength;
	}

	private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
	private long bytes_read = 0;
	private int tagLength = 0;
	private final byte buffer[] = new byte[1024];

	private boolean read()
	{
		try
		{
			if (is.available() < 0)
			{
				streamComplete = true;
				return true;
			}
			if (!async && is.available() < 1)
			{
				streamComplete = true;
				return true;
			}

			if (is.available() < 1)
				return false;

			{
				int read = is.read(buffer);
				if (read < 1)
				{
					setError("unexpected stream closed");
					return true;
				}

				baos.write(buffer, 0, read);
				bytes_read += read;
			}

			return true;
		} catch (IOException e)
		{
			// Debug.debug(e);
			setError(e.getMessage());
			return true;
		}
	}

	private String errorMessage = null;

	public String getErrorMessage()
	{
		return errorMessage;
	}

	private void setError(String s)
	{
		error = true;
		// Debug.debug("error", s);
		errorMessage = s;
	}

	public Vector getTags()
	{
		return frames;
	}

	public byte getVersionMajor()
	{
		return versionMajor;
	}

	public byte getVersionMinor()
	{
		return versionMinor;
	}

	public long getProgress()
	{
		return bytes_read;
	}

	public byte[] getBytes()
	{
		if (error || noTag || !complete)
			return null;

		byte bytes[] = baos.toByteArray();
		if (bytes.length < tagLength)
			return null;

		// Debug.debug("sought: " + tagLength);
		// Debug.debug("actually read: " + bytes.length);

		byte result[] = new byte[tagLength];
		System.arraycopy(bytes, 0, result, 0, tagLength);
		return result;
	}
}
